package com.stars.easyms.logger.support.logback.sift;

import ch.qos.logback.core.Appender;
import ch.qos.logback.core.Context;
import ch.qos.logback.core.CoreConstants;
import ch.qos.logback.core.helpers.NOPAppender;
import ch.qos.logback.core.joran.spi.JoranException;
import ch.qos.logback.core.sift.AppenderFactory;
import ch.qos.logback.core.spi.ComponentTracker;
import ch.qos.logback.core.spi.ContextAwareImpl;
import com.stars.easyms.logger.support.EasyMsOverride;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * <p>className: EasyMsAppenderTracker</p>
 * <p>description: 重写AppenderTracker类</p>
 *
 * @author guoguifang
 * @date 2019-08-22 19:48
 * @since 1.3.0
 */
public class EasyMsAppenderTracker<E> implements EasyMsOverride, ComponentTracker<Appender<E>> {

    private static final long LINGERING_TIMEOUT = 10000;

    private static final long WAIT_BETWEEN_SUCCESSIVE_REMOVAL_ITERATIONS = CoreConstants.MILLIS_IN_ONE_SECOND;

    private int maxComponents = DEFAULT_MAX_COMPONENTS;

    private long timeout = DEFAULT_TIMEOUT;

    private Map<String, Entry<Appender<E>>> liveMap = new ConcurrentHashMap<>(32);

    private Map<String, Entry<Appender<E>>> lingerersMap = new ConcurrentHashMap<>(16);

    private long lastCheck = 0;

    private int nopaWarningCount = 0;

    private final Context context;

    private final AppenderFactory<E> appenderFactory;

    private final ContextAwareImpl contextAware;

    public EasyMsAppenderTracker(Context context, AppenderFactory<E> appenderFactory) {
        super();
        this.context = context;
        this.appenderFactory = appenderFactory;
        this.contextAware = new ContextAwareImpl(context, this);
    }

    private void processPriorToRemoval(Appender<E> component) {
        component.stop();
    }

    private Appender<E> buildComponent(String key) {
        Appender<E> appender = null;
        try {
            appender = appenderFactory.buildAppender(context, key);
        } catch (JoranException je) {
            contextAware.addError("Error while building appender with discriminating value [" + key + "]");
        }
        if (appender == null) {
            appender = buildNOPAppender(key);
        }

        return appender;
    }

    private NOPAppender<E> buildNOPAppender(String key) {
        if (nopaWarningCount < CoreConstants.MAX_ERROR_COUNT) {
            nopaWarningCount++;
            contextAware.addError("Building NOPAppender for discriminating value [" + key + "]");
        }
        NOPAppender<E> nopa = new NOPAppender<>();
        nopa.setContext(context);
        nopa.start();
        return nopa;
    }

    private boolean isComponentStale(Appender<E> appender) {
        return !appender.isStarted();
    }

    @Override
    public int getComponentCount() {
        return liveMap.size() + lingerersMap.size();
    }

    private Entry<Appender<E>> getFromEitherMap(String key) {
        Entry<Appender<E>> entry = liveMap.get(key);
        if (entry != null) {
            return entry;
        } else {
            return lingerersMap.get(key);
        }
    }

    @Override
    public Appender<E> find(String key) {
        Entry<Appender<E>> entry = getFromEitherMap(key);
        if (entry == null) {
            return null;
        } else {
            return entry.component;
        }
    }

    @Override
    public Appender<E> getOrCreate(String key, long timestamp) {
        Entry<Appender<E>> entry = getFromEitherMap(key);
        if (entry == null) {
            Appender<E> c = buildComponent(key);
            entry = new Entry<>(key, c, timestamp);
            liveMap.put(key, entry);
        } else {
            entry.setTimestamp(timestamp);
        }
        return entry.component;
    }

    @Override
    public void endOfLife(String key) {
        Entry<Appender<E>> entry = liveMap.remove(key);
        if (entry == null) {
            return;
        }
        lingerersMap.put(key, entry);
    }

    @Override
    public void removeStaleComponents(long now) {
        if (isTooSoonForRemovalIteration(now)) {
            return;
        }
        removeExcedentComponents();
        removeStaleComponentsFromMainMap(now);
        removeStaleComponentsFromLingerersMap(now);
    }

    private void removeExcedentComponents() {
        genericStaleComponentRemover(liveMap, 0, byExcedent);
    }

    private void removeStaleComponentsFromMainMap(long now) {
        genericStaleComponentRemover(liveMap, now, byTimeout);
    }

    private void removeStaleComponentsFromLingerersMap(long now) {
        genericStaleComponentRemover(lingerersMap, now, byLingering);
    }

    private void genericStaleComponentRemover(Map<String, Entry<Appender<E>>> map, long now, RemovalPredicator<Appender<E>> removalPredicator) {
        Iterator<Map.Entry<String, Entry<Appender<E>>>> iter = map.entrySet().iterator();
        while (iter.hasNext()) {
            Map.Entry<String, Entry<Appender<E>>> mapEntry = iter.next();
            Entry<Appender<E>> entry = mapEntry.getValue();
            if (removalPredicator.isSlatedForRemoval(entry, now)) {
                iter.remove();
                Appender<E> c = entry.component;
                processPriorToRemoval(c);
            } else {
                break;
            }
        }
    }

    private RemovalPredicator<Appender<E>> byExcedent = (entry, timestamp) -> liveMap.size() > maxComponents;

    private RemovalPredicator<Appender<E>> byTimeout = this::isEntryStale;

    private RemovalPredicator<Appender<E>> byLingering = this::isEntryDoneLingering;

    private boolean isTooSoonForRemovalIteration(long now) {
        if (lastCheck + WAIT_BETWEEN_SUCCESSIVE_REMOVAL_ITERATIONS > now) {
            return true;
        }
        lastCheck = now;
        return false;
    }

    private boolean isEntryStale(Entry<Appender<E>> entry, long now) {
        Appender<E> c = entry.component;
        if (isComponentStale(c)) {
            return true;
        }

        return ((entry.timestamp + timeout) < now);
    }

    private boolean isEntryDoneLingering(Entry<Appender<E>> entry, long now) {
        return ((entry.timestamp + LINGERING_TIMEOUT) < now);
    }

    @Override
    public Set<String> allKeys() {
        HashSet<String> allKeys = new HashSet<>(liveMap.keySet());
        allKeys.addAll(lingerersMap.keySet());
        return allKeys;
    }

    @Override
    public Collection<Appender<E>> allComponents() {
        List<Appender<E>> allComponents = new ArrayList<>();
        for (Entry<Appender<E>> e : liveMap.values()) {
            allComponents.add(e.component);
        }
        for (Entry<Appender<E>> e : lingerersMap.values()) {
            allComponents.add(e.component);
        }
        return allComponents;
    }

    public long getTimeout() {
        return timeout;
    }

    public void setTimeout(long timeout) {
        this.timeout = timeout;
    }

    public int getMaxComponents() {
        return maxComponents;
    }

    public void setMaxComponents(int maxComponents) {
        this.maxComponents = maxComponents;
    }

    private interface RemovalPredicator<C> {
        boolean isSlatedForRemoval(Entry<C> entry, long timestamp);
    }

    private static class Entry<C> {
        String key;
        C component;
        long timestamp;

        Entry(String k, C c, long timestamp) {
            this.key = k;
            this.component = c;
            this.timestamp = timestamp;
        }

        public void setTimestamp(long timestamp) {
            this.timestamp = timestamp;
        }

        @Override
        public int hashCode() {
            return key.hashCode();
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            @SuppressWarnings("unchecked") final Entry<C> other = (Entry<C>) obj;
            if (key == null) {
                if (other.key != null) {
                    return false;
                }
            } else if (!key.equals(other.key)) {
                return false;
            }
            return Objects.equals(component, other.component);
        }

        @Override
        public String toString() {
            return "(" + key + ", " + component + ")";
        }
    }
}